﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Text;
using Pahan.McuFusion.Common;
using Pahan.McuFusion.Common.Utils;

namespace Pahan.McuFusion.Spi
{
    [Serializable]
    public class SpiAdapter : ISpiAdapter
    {
        #region Contructors

        public SpiAdapter(ISpiInterface spi, SpiAdapterInfo info)
        {
            Assert.NotNull(spi, "spi");
            this.spi = spi;
            this.info = info;
        }

        #endregion

        #region Implementation of ISpiAdapter

        /// <summary>
        /// Performs input/output via SPI interface.
        /// </summary>
        /// <param name="input">Input bytes.</param>
        /// <returns>Output bytes.</returns>
        public IList<byte> PerformIO(IList<byte> input)
        {
            Assert.NotNullOrEmpty(input, "input");
            BitArray inputBits = PrepareInputArray(input);
            var output = new Stack<bool>(inputBits.Length);

            spi.Sck = false;
            GCLatencyMode oldMode = GCSettings.LatencyMode;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                GCSettings.LatencyMode = GCLatencyMode.LowLatency;
                for (int i = inputBits.Length - 1; i >= 0; i--)
                {
                    WaitHelper.Wait(microseconds);
                    bool miso = spi.Miso;
                    spi.Mosi = inputBits[i];
                    ToggleSCK();
                    output.Push(miso);
                }
            }
            finally
            {
                GCSettings.LatencyMode = oldMode;
            }
            return PrepareOutputArray(new BitArray(output.ToArray()));
        }

        #endregion
        
        #region Public Properties

        [DisplayName("Delay, us")]
        [Description("Delay between SCK pulses in microseconds.")]
        public int Delay
        {
            get
            {
                return microseconds;
            }
            set
            {
                microseconds = value;
            }
        } 
        
        #endregion

        #region Private Members

        private BitArray PrepareInputArray(IList<byte> input)
        {
            byte[] bytes = new byte[input.Count];
            for (int i = bytes.Length - 1; i >= 0; i--)
            {
                bytes[i] = input[bytes.Length - 1 - i];
            }
            return new BitArray(bytes);
        }

        private IList<Byte> PrepareOutputArray(BitArray output)
        {
            byte[] bytes = new byte[(output.Length + 7) / 8];
            output.CopyTo(bytes, 0);
            Array.Reverse(bytes);
            return bytes;
        }

        private void ToggleSCK()
        {
            spi.Sck = true;
            spi.Sck = false;
        }

        private readonly ISpiInterface spi;

        private readonly HardwareAdapterInfo info;

        private int microseconds = 200;

        #endregion

        #region Implementation of IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Close();
        }

        #endregion

        #region Implementation of IHardwareAdapter

        /// <summary>
        /// Gets the information about the adapter.
        /// </summary>
        public HardwareAdapterInfo Info
        {
            get
            {
                return info;
            }
        }

        /// <summary>
        /// Opens the adapter. This method must be called before the actual use of the adapter.
        /// </summary>
        public void Open()
        {
            spi.Open();
            spi.Sck = false;
            spi.Reset = false;
            System.Threading.Thread.SpinWait(1000000);
            spi.Reset = true;
        }

        /// <summary>
        /// Closes the adapter.
        /// </summary>
        public void Close()
        {
            spi.Close();
        }

        #endregion
    }
}
